/**
 * Created by Neville Kadwa.
 */

package org.shiftone.cdep;

import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.util.Enumeration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.log4j.Logger;
import org.shiftone.cdep.util.JarFileNameFilter;
import org.shiftone.cdep.util.JavaClassFileNameFilter;
import org.shiftone.cdep.util.JavaContentFileNameFilter;
import org.shiftone.cdep.store.StoreException;
import org.shiftone.cdep.store.StoreManager;
import org.shiftone.cdep.store.StoreManagerFactory;
import org.shiftone.cdep.model.ClassInfo;
import org.shiftone.cdep.model.MethodInfo;
import org.shiftone.cdep.model.PackageInfo;
import org.shiftone.cdep.asm.CDepClassVisitor;
import org.objectweb.asm.ClassReader;

public class CDepLoad
{

    public static final Logger LOG = Logger.getLogger(CDepLoad.class);

    private JavaContentFileNameFilter javaContentFileNameFilter = new JavaContentFileNameFilter();
    private JarFileNameFilter         jarFileNameFilter         = new JarFileNameFilter();
    private JavaClassFileNameFilter   javaClassFileNameFilter   = new JavaClassFileNameFilter  ();

    private StoreManager     storeManager;
    private ArrayList        classes        = new ArrayList();
    private PackageManager   packageManager = new PackageManager();
    private CDepClassVisitor classVisitor   = new CDepClassVisitor();

    public CDepLoad() throws Exception
    {
        Runtime runtime = Runtime.getRuntime();
        LOG.info("Initial Memory State: " + (runtime.totalMemory()-runtime.freeMemory()) + " used.");
        storeManager = StoreManagerFactory.getStoreManager();

        classVisitor.addClassNameListener(new ClassNameListener() {
            public void encounterClassName(String className)
            {
                packageManager.addEntry(className);
            }
        });
    }

    private void processPackages(PackageInfo thePackage) throws StoreException
    {
        Integer parentId = null;
        if (thePackage.getParent() != null)
        {
            parentId = new Integer(thePackage.getParent().getPackageId());
        }

        int packageId = storeManager.setPackage(thePackage.getPackageName(), parentId);
        thePackage.setPackageId(packageId);
        LOG.debug("Adding package: " + thePackage.getPackageName() + "(" + thePackage.getPackageId() + ") parent " + parentId);

        if (thePackage.hasChildren())
        {
            Iterator iter = thePackage.getChildrenIterator();

            while (iter.hasNext())
            {
                PackageInfo childPackage = (PackageInfo) iter.next();
                processPackages(childPackage);
            }
        }
    }

    public void doProcessing() throws IOException, StoreException
    {
        LOG.info("Starting Processing of " + classes.size() + " Classes");
        Runtime runtime = Runtime.getRuntime();
        LOG.info("Memory State: " + (runtime.totalMemory()-runtime.freeMemory()) + " used.");

        PackageInfo[] rootPackages = packageManager.getRootPackages();
        for (int i = 0; i < rootPackages.length; i++)
        {
            LOG.debug("Loading root package: " + rootPackages[i].getPackageName());
            PackageInfo rootPackage = rootPackages[i];
            processPackages(rootPackage);
        }
        storeManager.buildPackageTree();

        LOG.info("I1 Memory State: " + (runtime.totalMemory()-runtime.freeMemory()) + " used.");

        Iterator iter = classes.iterator();
        while (iter.hasNext())
        {
            ClassInfo classDep = (ClassInfo) iter.next();
            if (false && LOG.isDebugEnabled())
            {
                LOG.debug("PKG: " + classDep.getPackageInfo().getPackageName() + "(" + classDep.getPackageInfo().getPackageId() + ")");
                LOG.debug("CLASS: " + classDep.getClassName() + " - " + classDep.getSuperClassName() + " at " + classDep.getImportLocation());
                Iterator implIter = classDep.getImplementsClasses();
                while (implIter.hasNext())
                {
                    LOG.debug("    IMPL: " + implIter.next());
                }
                Iterator ccIter = classDep.getClassCalls();
                while (ccIter.hasNext())
                {
                    LOG.debug("    CALL: " + ccIter.next());
                }
                Iterator mIter = classDep.getMethods();
                while (mIter.hasNext())
                {
                    MethodInfo methodDep = (MethodInfo) mIter.next();
                    LOG.debug("  METHOD: " + methodDep.getName() + methodDep.getSignature());
                    Iterator mcIter = methodDep.getMethodCalls();
                    while (mcIter.hasNext())
                    {
                        MethodInfo.MethodCallInfo methodCallInfo = (MethodInfo.MethodCallInfo) mcIter.next();
                        LOG.debug("    INVOKES: " + methodCallInfo.callClass + "." + methodCallInfo.method + methodCallInfo.signature);
                    }
                }
            }

            /* Iteration 1 - add all classes with no parent class and it's methods, remove methods that have no calls */
            int importLocationId = storeManager.setImportLocation(classDep.getImportLocation());

            if (classDep.getSuperClassName() == null)
            {
                Integer packageId = null;
                if (classDep.getPackageInfo() != null)
                    packageId = new Integer(classDep.getPackageInfo().getPackageId());
                int classId = storeManager.setClass(classDep.getClassName(), packageId,
                                                    Boolean.valueOf(classDep.isInterface()), Boolean.valueOf(classDep.isInner()),
                                                    null, new Integer(importLocationId));
                classDep.setClassId(classId);

                Iterator mIter = classDep.getMethods();
                while (mIter.hasNext())
                {
                    MethodInfo methodDep = (MethodInfo) mIter.next();
                    int methodId = storeManager.setMethod(classId, methodDep.getName(), methodDep.getSignature());
                    methodDep.setMethodId(methodId);
                    if (!methodDep.hasMethodCalls())
                    {
                        mIter.remove();
                    }
                }
            }
        }

        LOG.info("I2 Memory State: " + (runtime.totalMemory()-runtime.freeMemory()) + " used.");

        /* Iteration 2 - add remaining classes, create parent classes if not existing */
        iter = classes.iterator();
        while (iter.hasNext())
        {
            ClassInfo classDep = (ClassInfo) iter.next();

            int importLocationId = storeManager.setImportLocation(classDep.getImportLocation());

            if (classDep.getClassId() == 0)
            {
                int parentId = storeManager.setUnknownClass(classDep.getSuperClassName(), packageManager.getPackageId(classDep.getSuperClassName()));
                Integer packageId = null;
                if (classDep.getPackageInfo() != null)
                    packageId = new Integer(classDep.getPackageInfo().getPackageId());
                int classId = storeManager.setClass(classDep.getClassName(), packageId,
                                                    Boolean.valueOf(classDep.isInterface()), Boolean.valueOf(classDep.isInner()),
                                                    new Integer(parentId), new Integer(importLocationId));
                classDep.setClassId(classId);

                Iterator mIter = classDep.getMethods();
                while (mIter.hasNext())
                {
                    MethodInfo methodDep = (MethodInfo) mIter.next();
                    int methodId = storeManager.setMethod(classId, methodDep.getName(), methodDep.getSignature());
                    methodDep.setMethodId(methodId);
                    if (!methodDep.hasMethodCalls())
                    {
                        mIter.remove();
                    }
                }
            }
        }

        LOG.info("I3 Memory State: " + (runtime.totalMemory()-runtime.freeMemory()) + " used.");
        /* Iteration 3 - add implements classes, class calls, create method calls records */
        iter = classes.iterator();
        while (iter.hasNext())
        {
            ClassInfo classDep = (ClassInfo) iter.next();

            if (classDep.hasImplementsClasses())
            {
                Iterator icIter = classDep.getImplementsClasses();
                while (icIter.hasNext())
                {
                    String s = (String) icIter.next();
                    int ifaceId = storeManager.setUnknownClass(s, packageManager.getPackageId(s));
                    storeManager.setClassImplements(classDep.getClassId(), ifaceId);
                }

                Iterator ccIter = classDep.getClassCalls();
                while (ccIter.hasNext())
                {
                    String s = (String) ccIter.next();
                    int caleeClassId = storeManager.setUnknownClass(s, packageManager.getPackageId(s));
                    storeManager.setClassCall(classDep.getClassId(), caleeClassId);
                }
            }

            Iterator mIter = classDep.getMethods();
            while (mIter.hasNext())
            {
                MethodInfo methodDep = (MethodInfo) mIter.next();

                Iterator mcIter = methodDep.getMethodCalls();
                while (mcIter.hasNext())
                {
                    MethodInfo.MethodCallInfo methodCallInfo = (MethodInfo.MethodCallInfo) mcIter.next();
                    int caleeClassId = storeManager.setUnknownClass(methodCallInfo.callClass, packageManager.getPackageId(methodCallInfo.callClass));
                    int caleeMethodId = storeManager.setMethod(caleeClassId, methodCallInfo.method, methodCallInfo.signature);
//                    if (caleeMethodId == -1)
//                    {
//                        storeManager.setClassCall(classDep.getClassId(), caleeClassId);
//                    } else {
                        storeManager.setMethodCall(methodDep.getMethodId(), caleeMethodId);
//                    }
                }
            }
            iter.remove();
        }
        LOG.info("END Memory State: " + (runtime.totalMemory()-runtime.freeMemory()) + " used.");

        storeManager.shutdown();

        LOG.info("Succeeded!");
    }

    public void processClassFile(InputStream classStream, String className, String location) throws IOException, StoreException
    {
        LOG.debug("Loading Class File: " + className);

        classVisitor.setLocation(location);

        ClassReader cr = new ClassReader(classStream);
        cr.accept(classVisitor, ClassReader.SKIP_DEBUG);

        ClassInfo classInfo = classVisitor.getClassDependencyInfo();
        classes.add(classInfo);
        PackageInfo packageInfo = packageManager.addEntry(classInfo.getClassName());
        classInfo.setPackageInfo(packageInfo);
    }

    public void processClassPath(ZipFile zipFile, String location) throws IOException, StoreException
    {
        LOG.debug("Processing Zip File: " + zipFile.getName());

        if (!(new JarFileNameFilter().accept(null, zipFile.getName())))
        {
            throw new RuntimeException("Entry failed filter: " + zipFile.getName());
        }

        Enumeration enumer = zipFile.entries();

        while (enumer.hasMoreElements())
        {
            ZipEntry zipEntry = (ZipEntry) enumer.nextElement();

            if (!zipEntry.isDirectory())
            {
                if (jarFileNameFilter.accept(null, zipEntry.getName()))
                {
                    LOG.debug("Processing internal zip: " + zipEntry.getName());
                    File temp = File.createTempFile("cdep", ".zip");
                    OutputStream os = new FileOutputStream(temp);
                    InputStream is = zipFile.getInputStream(zipEntry);
                    byte[] buf = new byte[2048];
                    int numRead = is.read(buf);
                    while (numRead != -1)
                    {
                        os.write(buf, 0, numRead);
                        numRead = is.read(buf);
                    }
                    os.close();
                    processClassPath(new ZipFile(temp), location + "!" + zipEntry.getName());
                    temp.deleteOnExit();
                }
                else
                if (javaClassFileNameFilter.accept(null, zipEntry.getName()))
                {
                    processClassFile(zipFile.getInputStream(zipEntry), zipEntry.getName(), location);
                }
                else
                {
                    LOG.debug("Unable to process zip entry: " + zipEntry.getName());
                }
            } else {
//                LOG.error(zipEntry.getName() + " is a directory.. what do I do?");
            }

        }
    }

    public void processClassPath(File directory, String location) throws IOException, StoreException
    {
        LOG.debug("Processing Class Path: " + directory.getAbsolutePath());

        File[] files = directory.listFiles();
        for (int i = 0; i < files.length; i++)
        {
            if (files[i].isDirectory())
            {
                processClassPath(files[i], location);
            } else if (javaContentFileNameFilter.accept(null, files[i].getName()))
            {
                processFileEntry(files[i], location);
            } else {
                LOG.warn("Unable to process: " + files[i]);
            }
        }
    }

    public void processFileEntry(File file, String location) throws StoreException, IOException
    {
        if (file.exists())
        {
            if (file.isDirectory())
            {
                processClassPath(file, location);
            }
            else if (jarFileNameFilter.accept(file, file.getName()))
            {
                processClassPath(new ZipFile(file, ZipFile.OPEN_READ), /*location + "!" + */file.getAbsolutePath());
            }
            else if (javaClassFileNameFilter.accept(file, file.getName()))
            {
                processClassFile(new FileInputStream(file), file.getAbsolutePath(), location);
            }
            else
            {
                System.err.println("ERROR: Unable to determine: " + file.getAbsolutePath());
            }
        } else {
            System.err.println("NOT EXISTS: " + file.getPath());
        }
    }

    public void processZipEntry(ZipFile zipFile, ZipEntry zipEntry)
    {
    }

    public void processEntry(String entry)
    {
    }

    public static void main(String[] args)
    {
        if (args.length == 0)
        {
            System.out.println("Usage: CDepLoad { [jar file | classpath | class file] ... }");
            System.exit(-1);
        }

        System.out.println("Log4J Configured from: " + System.getProperty("log4j.configuration"));

        try
        {
            CDepLoad cDepLoad = new CDepLoad();

            for (int i = 0; i < args.length; i++)
            {
                File file = new File(args[i]);

                cDepLoad.processFileEntry(file, cDepLoad.getDirectory(file));

            }

            cDepLoad.doProcessing();
            System.exit(0);
        }
        catch (Exception ex)
        {
            System.err.println("Exception: ");
            ex.printStackTrace();
            System.exit(-1);
        }
    }

    private String getDirectory(File location)
    {
        if (location.isDirectory())
            return location.getAbsolutePath();
        String absoluteLoc = location.getAbsolutePath();
        int locationIndex = absoluteLoc.lastIndexOf(File.separator);
        return absoluteLoc.substring(0, locationIndex);
    }
}

/* EOF */
